package org.elasticsearch.http.netty4;

import org.elasticsearch.HttpPipelinedRequest;
import org.elasticsearch.Netty4HttpServerTransport;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.rest.AbstractRestChannel;
import org.elasticsearch.rest.RestStatus;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.*;

import java.util.*;

final class Netty4HttpChannel extends AbstractRestChannel {

    private final Netty4HttpServerTransport transport;
    private final Channel channel;
    private final FullHttpRequest nettyRequest;
    private final HttpPipelinedRequest pipelinedRequest;

    Netty4HttpChannel(
            final Netty4HttpServerTransport transport,
            final Netty4HttpRequest request,
            final HttpPipelinedRequest pipelinedRequest,
            final boolean detailedErrorsEnabled) {
        super(request, detailedErrorsEnabled);
        this.transport = transport;
        this.channel = request.getChannel();
        this.nettyRequest = request.request();
        this.pipelinedRequest = pipelinedRequest;
    }

    @Override
    public BytesStreamOutput newBytesOutput() {
//        return new ReleasableBytesStreamOutput(transport.bigArrays);
        return null;
    }

//    @Override
//    public void sendResponse(RestResponse response) {
//        // if the response object was created upstream, then use it;
//        // otherwise, create a new one
//        ByteBuf buffer = Netty4Utils.toByteBuf(response.content());
//        final FullHttpResponse resp;
//        if (HttpMethod.HEAD.equals(nettyRequest.method())) {
//            resp = newResponse(Unpooled.EMPTY_BUFFER);
//        } else {
//            resp = newResponse(buffer);
//        }
//        resp.setStatus(getStatus(response.status()));
//
//        Netty4CorsHandler.setCorsResponseHeaders(nettyRequest, resp, transport.getCorsConfig());
//
//        String opaque = nettyRequest.headers().get("X-Opaque-Id");
//        if (opaque != null) {
//            setHeaderField(resp, "X-Opaque-Id", opaque);
//        }
//
//        // Add all custom headers
//        addCustomHeaders(resp, response.getHeaders());
//        addCustomHeaders(resp, threadContext.getResponseHeaders());
//
//        BytesReference content = response.content();
//        boolean release = content instanceof Releasable;
//        try {
//            // If our response doesn't specify a content-type header, set one
//            setHeaderField(resp, HttpHeaderNames.CONTENT_TYPE.toString(), response.contentType(), false);
//            // If our response has no content-length, calculate and set one
//            setHeaderField(resp, HttpHeaderNames.CONTENT_LENGTH.toString(), String.valueOf(buffer.readableBytes()), false);
//
//            addCookies(resp);
//
//            final ChannelPromise promise = channel.newPromise();
//
//            if (release) {
//                promise.addListener(f -> ((Releasable)content).close());
//            }
//
//            if (isCloseConnection()) {
//                promise.addListener(ChannelFutureListener.CLOSE);
//            }
//
//            final Object msg;
//            if (pipelinedRequest != null) {
//                msg = pipelinedRequest.createHttpResponse(resp, promise);
//            } else {
//                msg = resp;
//            }
//            channel.writeAndFlush(msg, promise);
//            release = false;
//        } finally {
//            if (release) {
//                ((Releasable) content).close();
//            }
//            if (pipelinedRequest != null) {
//                pipelinedRequest.release();
//            }
//        }
//    }

    private void setHeaderField(HttpResponse resp, String headerField, String value) {
        setHeaderField(resp, headerField, value, true);
    }

    private void setHeaderField(HttpResponse resp, String headerField, String value, boolean override) {
        if (override || !resp.headers().contains(headerField)) {
            resp.headers().add(headerField, value);
        }
    }


    private void addCustomHeaders(HttpResponse response, Map<String, List<String>> customHeaders) {
        if (customHeaders != null) {
            for (Map.Entry<String, List<String>> headerEntry : customHeaders.entrySet()) {
                for (String headerValue : headerEntry.getValue()) {
                    setHeaderField(response, headerEntry.getKey(), headerValue);
                }
            }
        }
    }

    // Determine if the request protocol version is HTTP 1.0
    private boolean isHttp10() {
        return nettyRequest.protocolVersion().equals(HttpVersion.HTTP_1_0);
    }

    // Determine if the request connection should be closed on completion.
    private boolean isCloseConnection() {
        final boolean http10 = isHttp10();
        return HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(nettyRequest.headers().get(HttpHeaderNames.CONNECTION)) ||
                (http10 && !HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(nettyRequest.headers().get(HttpHeaderNames.CONNECTION)));
    }

    // Create a new {@link HttpResponse} to transmit the response for the netty request.
    private FullHttpResponse newResponse(ByteBuf buffer) {
        final boolean http10 = isHttp10();
        final boolean close = isCloseConnection();
        // Build the response object.
        final HttpResponseStatus status = HttpResponseStatus.OK; // default to initialize
        final FullHttpResponse response;
        if (http10) {
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, status, buffer);
            if (!close) {
                response.headers().add(HttpHeaderNames.CONNECTION, "Keep-Alive");
            }
        } else {
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buffer);
        }
        return response;
    }

    private static final HttpResponseStatus TOO_MANY_REQUESTS = new HttpResponseStatus(429, "Too Many Requests");

    private static Map<RestStatus, HttpResponseStatus> MAP;

    static {
        EnumMap<RestStatus, HttpResponseStatus> map = new EnumMap<>(RestStatus.class);
        map.put(RestStatus.CONTINUE, HttpResponseStatus.CONTINUE);
        map.put(RestStatus.SWITCHING_PROTOCOLS, HttpResponseStatus.SWITCHING_PROTOCOLS);
        map.put(RestStatus.OK, HttpResponseStatus.OK);
        map.put(RestStatus.CREATED, HttpResponseStatus.CREATED);
        map.put(RestStatus.ACCEPTED, HttpResponseStatus.ACCEPTED);
        map.put(RestStatus.NON_AUTHORITATIVE_INFORMATION, HttpResponseStatus.NON_AUTHORITATIVE_INFORMATION);
        map.put(RestStatus.NO_CONTENT, HttpResponseStatus.NO_CONTENT);
        map.put(RestStatus.RESET_CONTENT, HttpResponseStatus.RESET_CONTENT);
        map.put(RestStatus.PARTIAL_CONTENT, HttpResponseStatus.PARTIAL_CONTENT);
        map.put(RestStatus.MULTI_STATUS, HttpResponseStatus.INTERNAL_SERVER_ERROR); // no status for this??
        map.put(RestStatus.MULTIPLE_CHOICES, HttpResponseStatus.MULTIPLE_CHOICES);
        map.put(RestStatus.MOVED_PERMANENTLY, HttpResponseStatus.MOVED_PERMANENTLY);
        map.put(RestStatus.FOUND, HttpResponseStatus.FOUND);
        map.put(RestStatus.SEE_OTHER, HttpResponseStatus.SEE_OTHER);
        map.put(RestStatus.NOT_MODIFIED, HttpResponseStatus.NOT_MODIFIED);
        map.put(RestStatus.USE_PROXY, HttpResponseStatus.USE_PROXY);
        map.put(RestStatus.TEMPORARY_REDIRECT, HttpResponseStatus.TEMPORARY_REDIRECT);
        map.put(RestStatus.BAD_REQUEST, HttpResponseStatus.BAD_REQUEST);
        map.put(RestStatus.UNAUTHORIZED, HttpResponseStatus.UNAUTHORIZED);
        map.put(RestStatus.PAYMENT_REQUIRED, HttpResponseStatus.PAYMENT_REQUIRED);
        map.put(RestStatus.FORBIDDEN, HttpResponseStatus.FORBIDDEN);
        map.put(RestStatus.NOT_FOUND, HttpResponseStatus.NOT_FOUND);
        map.put(RestStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.METHOD_NOT_ALLOWED);
        map.put(RestStatus.NOT_ACCEPTABLE, HttpResponseStatus.NOT_ACCEPTABLE);
        map.put(RestStatus.PROXY_AUTHENTICATION, HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED);
        map.put(RestStatus.REQUEST_TIMEOUT, HttpResponseStatus.REQUEST_TIMEOUT);
        map.put(RestStatus.CONFLICT, HttpResponseStatus.CONFLICT);
        map.put(RestStatus.GONE, HttpResponseStatus.GONE);
        map.put(RestStatus.LENGTH_REQUIRED, HttpResponseStatus.LENGTH_REQUIRED);
        map.put(RestStatus.PRECONDITION_FAILED, HttpResponseStatus.PRECONDITION_FAILED);
        map.put(RestStatus.REQUEST_ENTITY_TOO_LARGE, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
        map.put(RestStatus.REQUEST_URI_TOO_LONG, HttpResponseStatus.REQUEST_URI_TOO_LONG);
        map.put(RestStatus.UNSUPPORTED_MEDIA_TYPE, HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE);
        map.put(RestStatus.REQUESTED_RANGE_NOT_SATISFIED, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
        map.put(RestStatus.EXPECTATION_FAILED, HttpResponseStatus.EXPECTATION_FAILED);
        map.put(RestStatus.UNPROCESSABLE_ENTITY, HttpResponseStatus.BAD_REQUEST);
        map.put(RestStatus.LOCKED, HttpResponseStatus.BAD_REQUEST);
        map.put(RestStatus.FAILED_DEPENDENCY, HttpResponseStatus.BAD_REQUEST);
        map.put(RestStatus.TOO_MANY_REQUESTS, TOO_MANY_REQUESTS);
        map.put(RestStatus.INTERNAL_SERVER_ERROR, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        map.put(RestStatus.NOT_IMPLEMENTED, HttpResponseStatus.NOT_IMPLEMENTED);
        map.put(RestStatus.BAD_GATEWAY, HttpResponseStatus.BAD_GATEWAY);
        map.put(RestStatus.SERVICE_UNAVAILABLE, HttpResponseStatus.SERVICE_UNAVAILABLE);
        map.put(RestStatus.GATEWAY_TIMEOUT, HttpResponseStatus.GATEWAY_TIMEOUT);
        map.put(RestStatus.HTTP_VERSION_NOT_SUPPORTED, HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED);
        MAP = Collections.unmodifiableMap(map);
    }

    private static HttpResponseStatus getStatus(RestStatus status) {
        return MAP.getOrDefault(status, HttpResponseStatus.INTERNAL_SERVER_ERROR);
    }

}
